﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedParameter.Local

/* ExemplarManager.cs -- работа с экземплярами книги/журнала/газеты/подшивки и т. д.
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

using AM;
using AM.IO;
using AM.Runtime;
using AM.Text.Output;

using ManagedIrbis.Batch;
using ManagedIrbis.Readers;
using ManagedIrbis.Providers;

using Microsoft.Extensions.Logging;

#endregion

#nullable enable

namespace ManagedIrbis.Fields;

/// <summary>
/// Работа с экземплярами книги/журнала/газеты/подшивки и т. д.
/// Manages exemplars of the books/magazines etc.
/// </summary>
public sealed class ExemplarManager
{
    #region Properties

    /// <summary>
    /// Провайдер данных.
    /// </summary>
    public ISyncProvider Connection { get; }

    /// <summary>
    /// Формат краткого библиографического описания.
    /// </summary>
    public string Format { get; set; }

    /// <summary>
    /// Список экземпляров.
    /// </summary>
    public ReadOnlyCollection<ExemplarInfo> List => _list.AsReadOnly();

    /// <summary>
    /// Выходной поток.
    /// </summary>
    public AbstractOutput? Output => _output;

    /// <summary>
    /// Префикс.
    /// </summary>
    public string Prefix { get; set; }

    #endregion

    #region Construction

    /// <summary>
    /// Конструктор.
    /// </summary>
    public ExemplarManager
        (
            ISyncProvider connection,
            AbstractOutput? output = null
        )
    {
        Sure.NotNull (connection);

        Connection = connection;
        _output = output;
        Prefix = "IN=";
        Format = "@brief";
        _list = new List<ExemplarInfo>();
        _newspapers = new Dictionary<string, bool>();
    }

    #endregion

    #region Private members

    private readonly List<ExemplarInfo> _list;

    private readonly Dictionary<string, bool> _newspapers;

    private readonly AbstractOutput? _output;

    #endregion

    #region Public methods

    /// <summary>
    /// Получение года издания.
    /// </summary>
    public static string GetYear
        (
            Record record
        )
    {
        Sure.NotNull (record);

        var result = record.FM (210, 'd');
        if (result.IsEmpty())
        {
            result = record.FM (461, 'h');
        }

        if (result.IsEmpty())
        {
            result = record.FM (461, 'z');
        }

        if (result.IsEmpty())
        {
            var worksheet = record.FM (920);
            if (worksheet.SameString ("NJ") // отдельный номер журнала
                || worksheet.SameString ("NJK") // подшивка
                || worksheet.SameString ("NJP")) // номер, входящий в подшивку
            {
                result = record.FM (934);
            }
        }

        if (result.IsEmpty())
        {
            return result ?? string.Empty;
        }

        var match = Regex.Match (result, @"\d{4}");
        if (match.Success)
        {
            result = match.Value;
        }

        return result;
    }

    /// <summary>
    /// Получение цены экземпляра.
    /// </summary>
    public static string GetPrice
        (
            Record record,
            ExemplarInfo exemplar
        )
    {
        Sure.NotNull (record);
        Sure.NotNull (exemplar);

        if (!exemplar.Price.IsEmpty())
        {
            return exemplar.Price;
        }

        var price = record.FM (10, 'd');

        return !price.IsEmpty() ? price : string.Empty;
    }

    /// <summary>
    /// Add given exemplar to the collection.
    /// </summary>
    public ExemplarManager Add
        (
            ExemplarInfo exemplar
        )
    {
        Sure.NotNull (exemplar);

        if (string.IsNullOrEmpty (exemplar.Number))
        {
            return this;
        }

        if (Find (exemplar.Number) == null)
        {
            _list.Add (exemplar);
        }

        return this;
    }

    /// <summary>
    /// Add many.
    /// </summary>
    public ExemplarManager AddRange
        (
            IEnumerable<ExemplarInfo> exemplars
        )
    {
        Sure.NotNull (exemplars);

        foreach (var exemplar in exemplars)
        {
            Add (exemplar);
        }

        return this;
    }

    /// <summary>
    /// Очистка списка экземпляров.
    /// </summary>
    public ExemplarManager Clear()
    {
        _list.Clear();

        return this;
    }

    /// <summary>
    /// Получение библиографического описания для указанной записи.
    /// </summary>
    public string GetDescription
        (
            Record record
        )
    {
        Sure.NotNull (record);

        var result = record.Description;
        if (string.IsNullOrEmpty (result))
        {
            result = Connection.FormatRecord
                (
                    Format,
                    record.Mfn
                );
            record.Description = result;
        }

        if (string.IsNullOrEmpty (result))
        {
            Magna.Logger.LogError
                (
                    nameof (ExemplarManager) + "::" + nameof (GetDescription)
                    + ": empty description"
                );

            throw new IrbisException ("Empty description");
        }

        return result;
    }

    /// <summary>
    /// Получение библиографического описания для указанного экземпляра.
    /// </summary>
    public string? GetDescription
        (
            Record? record,
            ExemplarInfo exemplar
        )
    {
        Sure.NotNull (exemplar);

        string? result;

        if (!ReferenceEquals (record, null))
        {
            result = GetDescription (record);
        }
        else
        {
            result = Connection.FormatRecord
                (
                    Format,
                    exemplar.Mfn
                );
        }

        return result;
    }

    /// <summary>
    /// Получение расширенной информации об указанном экземпляре.
    /// </summary>
    public ExemplarInfo Extend
        (
            ExemplarInfo exemplar,
            Record? record
        )
    {
        Sure.NotNull (exemplar);

        if (exemplar.Mfn <= 0)
        {
            Magna.Logger.LogError
                (
                    nameof (ExemplarManager) + "::" + nameof (Extend)
                    + ": MFN={Mfn}",
                    exemplar.Mfn
                );

            throw new IrbisException ("MFN <= 0");
        }

        exemplar.Description = GetDescription
            (
                record,
                exemplar
            );

        if (!ReferenceEquals (record, null))
        {
            var workList = record.FM (920);

            if (string.IsNullOrEmpty (exemplar.ShelfIndex))
            {
                exemplar.ShelfIndex = Utility.NonEmpty
                    (
                        record.FM (906),
                        record.FM (621),
                        record.FM (686)
                    );
            }

            if (exemplar.ShelfIndex.IsEmpty()
                && workList.SameString ("NJ"))
            {
                var consolidatedIndex = record.FM (933);
                if (!string.IsNullOrEmpty (consolidatedIndex))
                {
                    var expression = $"\"I={consolidatedIndex}\"";
                    var consolidatedRecord = Connection.SearchReadOneRecord (expression);
                    if (consolidatedRecord is not null)
                    {
                        exemplar.ShelfIndex = Utility.NonEmpty
                            (
                                consolidatedRecord.FM (906),
                                consolidatedRecord.FM (621),
                                consolidatedRecord.FM (686)
                            );
                    }
                }
            }

            exemplar.Year = GetYear (record);
            exemplar.Price = GetPrice (record, exemplar);
            exemplar.Issue = record.FM (936);
        }

        return exemplar;
    }

    /// <summary>
    /// Поиск экземпляра по его номеру.
    /// </summary>
    public ExemplarInfo? Find
        (
            string? number
        )
    {
        if (string.IsNullOrEmpty (number))
        {
            return null;
        }

        return _list.FirstOrDefault
            (
                e => e.Number.SameString (number)
                     || e.Barcode.SameString (number)
            );
    }

    /// <summary>
    /// Разбор библиографической записи на экземпляры.
    /// </summary>
    public ExemplarInfo[] FromRecord
        (
            Record record
        )
    {
        Sure.NotNull (record);

        var result = ExemplarInfo.ParseRecord (record);

        foreach (var exemplar in result)
        {
            Extend (exemplar, record);
        }

        return result;
    }

    /// <summary>
    /// Determines whether the record is newspaper/magazine or not.
    /// </summary>
    public bool IsNewspaper
        (
            Record record
        )
    {
        Sure.NotNull (record);

        var worklist = record.FM (920);
        if (worklist.IsEmpty())
        {
            return false;
        }

        if (!worklist.SameString ("NJ"))
        {
            return false;
        }

        var index = record.FM (933);
        if (string.IsNullOrEmpty (index))
        {
            return false;
        }

        if (_newspapers.TryGetValue (index, out var result))
        {
            return result;
        }

        var main = Connection.SearchReadOneRecord ($"\"I={index}\"");
        if (ReferenceEquals (main, null))
        {
            return false;
        }

        var kind = main.FM (110, 'b');
        result = kind.SameString ("c");
        _newspapers[index] = result;
        return result;
    }

    /// <summary>
    /// List library places.
    /// </summary>
    public ChairInfo[] ListPlaces()
    {
        var result = ChairInfo.Read
                (
                    Connection,
                    "mhr.mnu",
                    false
                )
            .ToArray();

        return result;
    }

    /// <summary>
    /// Загрузка информации из файла.
    /// </summary>
    public void LoadFromFile
        (
            string fileName
        )
    {
        Sure.FileExists (fileName);

        var loaded = SerializationUtility
            .RestoreArrayFromFile<ExemplarInfo> (fileName);

        if (ReferenceEquals (loaded, null))
        {
            Magna.Logger.LogError
                (
                    "ExemplarManager::LoadFromFile: "
                    + "failed to load from: {FileName}",
                    fileName
                );

            throw new IrbisException
                (
                    "Failed to load exemplars from file"
                );
        }

        foreach (var exemplar in loaded)
        {
            if (string.IsNullOrEmpty (exemplar.Number))
            {
                continue;
            }

            var copy = Find (exemplar.Number);
            if (copy == null)
            {
                _list.Add (exemplar);
            }
        }
    }

    /// <summary>
    /// Reads exemplar for given number.
    /// </summary>
    public ExemplarInfo? Read
        (
            string number
        )
    {
        var record = Connection.SearchReadOneRecord ($"\"{Prefix}{number}\"");
        if (ReferenceEquals (record, null))
        {
            return null;
        }

        var fields = record.Fields.GetField (ExemplarInfo.ExemplarTag);
        foreach (var field in fields)
        {
            var result = ExemplarInfo.ParseField (field);
            if (result.Barcode.SameString (number)
                || result.Number.SameString (number))
            {
                result.Record = record;
                result.Mfn = record.Mfn;

                return result;
            }
        }

        return null;
    }

    /// <summary>
    /// Reads exemplar for given number.
    /// </summary>
    public ExemplarInfo? ReadExtend
        (
            string number
        )
    {
        var record = Connection.SearchReadOneRecord ($"\"{Prefix}{number}\"");
        if (ReferenceEquals (record, null))
        {
            return null;
        }

        var fields = record.Fields.GetField (ExemplarInfo.ExemplarTag);
        foreach (var field in fields)
        {
            var result = ExemplarInfo.ParseField (field);
            if (result.Barcode.SameString (number)
                || result.Number.SameString (number))
            {
                result.Record = record;
                result.Mfn = record.Mfn;
                Extend (result, record);

                return result;
            }
        }

        return null;
    }

    /// <summary>
    /// Read configuration.
    /// </summary>
    public void ReadConfiguration
        (
            string fileName,
            Encoding? encoding
        )
    {
        encoding ??= Encoding.UTF8;

        using var ini = new IniFile (fileName, encoding);
        var section = ini.GetSection ("Main");
        if (!ReferenceEquals (section, null))
        {
            var format = section.GetValue ("Format", Format);
            if (!string.IsNullOrEmpty (format))
            {
                Format = format;
            }

            var prefix = section.GetValue ("Prefix", Prefix);
            if (!string.IsNullOrEmpty (prefix))
            {
                Prefix = prefix;
            }
        }
    }

    /// <summary>
    /// Save the configuration.
    /// </summary>
    public void SaveConfiguration
        (
            string fileName,
            Encoding? encoding
        )
    {
        encoding ??= Encoding.UTF8;

        using var ini = File.Exists (fileName)
            ? new IniFile (fileName, encoding, true)
            : new IniFile { Encoding = encoding };
        var section = ini.GetOrCreateSection ("Main");
        section["Format"] = Format;
        section["Prefix"] = Prefix;

        ini.Save (fileName);
    }

    /// <summary>
    /// Save to the file.
    /// </summary>
    public void SaveToFile
        (
            string fileName
        )
    {
        _list.ToArray().SaveToZipFile (fileName);
    }

    /// <summary>
    /// Read many.
    /// </summary>
    public ExemplarManager ReadRange
        (
            string? place,
            string searchExpression
        )
    {
        var reader = BatchRecordReader.Search
            (
                Connection,
                Connection.Database!,
                searchExpression,
                1000
            );
        foreach (var record in reader)
        {
            var exemplars = FromRecord (record);
            if (!string.IsNullOrEmpty (place))
            {
                exemplars = exemplars
                    .Where (e => e.Place.SameString (place))
                    .ToArray();
            }

            foreach (var exemplar in exemplars)
            {
                Add (exemplar);
            }
        }

        return this;
    }

    /// <summary>
    /// Удаление номера из списка экземпляров.
    /// </summary>
    public ExemplarManager Remove
        (
            string? number
        )
    {
        if (string.IsNullOrEmpty (number))
        {
            return this;
        }

        var found = Find (number);
        if (found != null)
        {
            _list.Remove (found);
        }

        return this;
    }

    /// <summary>
    /// Write line.
    /// </summary>
    public void WriteLine
        (
            string format,
            params object[] args
        )
    {
        _output?.WriteLine (format, args);
    }

    /// <summary>
    /// Write delimiter.
    /// </summary>
    public void WriteDelimiter()
    {
        WriteLine (new string ('=', 60));
    }

    #endregion
}
